/**
 * chenuu.com
 * 
 * @author  zhaoxu.com@gmail.com
 * @created 2012-11-10
 */
package com.chenuu.integration.httpclient.dropbox;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.SyncFailedException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;

import com.dropbox.client2.ProgressListener;
import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.exception.DropboxIOException;
import com.dropbox.client2.exception.DropboxLocalStorageFullException;
import com.dropbox.client2.exception.DropboxPartialFileException;

/**
 *
 */
public class DropboxInputStream extends FilterInputStream {

    private final HttpUriRequest request;
    private final DropboxFileInfo info;

    public DropboxInputStream(HttpUriRequest request,
            HttpResponse response) throws DropboxException {
        // Give the FilterInputStream a null stream at first so we can
        // handle errors better.
        super(null);

        HttpEntity entity = response.getEntity();
        if (entity == null) {
            throw new DropboxException("Didn't get entity from HttpResponse");
        }

        // Now set the input stream on FilterInputStream. This will throw
        // an IOException itself if something goes wrong.
        try {
            in = entity.getContent();
        } catch (IOException e) {
            throw new DropboxIOException(e);
        }

        this.request = request;
        info = new DropboxFileInfo(response);
    }

    /**
     * Closes this stream and aborts the request to Dropbox, releasing
     * any associated resources. No more bytes will be downloaded after
     * this is called.
     *
     * @throws IOException if an error occurs while closing this stream.
     */
    @Override
    public void close() throws IOException {
        // Aborting the request also closes the input stream that it
        // creates (the in variable). Do not try to close it again.
        request.abort();
    }

    /**
     * Returns the {@link DropboxFileInfo} for the associated file.
     */
    public DropboxFileInfo getFileInfo() {
        return info;
    }


    /**
     * Copies from a {@link DropboxInputStream} to an
     * {@link OutputStream}, optionally providing updates via a
     * {@link ProgressListener}. You probably won't have a use for this
     * function because most API functions that return a
     * {@link DropboxInputStream} have an alternate that will copy to an
     * {@link OutputStream} for you.
     *
     * @param os the stream to copy to.
     * @param listener an optional {@link ProgressListener} to receive progress
     *         updates as the stream is copied, or null.
     *
     * @throws DropboxPartialFileException if only part of the input stream was
     *         copied.
     * @throws DropboxIOException for network-related errors.
     * @throws DropboxLocalStorageFullException if there is no more room to
     *         write to the output stream.
     * @throws DropboxException for any other unknown errors. This is also a
     *         superclass of all other Dropbox exceptions, so you may want to
     *         only catch this exception which signals that some kind of error
     *         occurred.
     */
    public void copyStreamToOutput(OutputStream os, ProgressListener listener)
            throws DropboxIOException, DropboxPartialFileException,
            DropboxLocalStorageFullException {
        BufferedOutputStream bos = null;
        long totalRead = 0;
        long lastListened = 0;
        long length = info.getFileSize();

        try {
            bos = new BufferedOutputStream(os);

            byte[] buffer = new byte[4096];
            int read;
            while (true) {
                read = read(buffer);
                if (read < 0) {
                    if (length >= 0 && totalRead < length) {
                        // We've reached the end of the file, but it's unexpected.
                        throw new DropboxPartialFileException(totalRead);
                    }
                    // TODO check for partial success, if possible
                    break;
                }

                bos.write(buffer, 0, read);

                totalRead += read;

                if (listener != null) {
                    long now = System.currentTimeMillis();
                    if (now - lastListened > listener.progressInterval()) {
                        lastListened = now;
                        listener.onProgress(totalRead, length);
                    }
                }
            }

            bos.flush();
            os.flush();
            // Make sure it's flushed out to disk
            try {
                if (os instanceof FileOutputStream) {
                    ((FileOutputStream)os).getFD().sync();
                }
            } catch (SyncFailedException e) {
            }

        } catch (IOException e) {
            String message = e.getMessage();
            if (message != null && message.startsWith("No space")) {
                // This is a hack, but it seems to be the only way to check
                // which exception it is.
                throw new DropboxLocalStorageFullException();
            } else {
                /*
                 * If the output stream was closed, we notify the caller
                 * that only part of the file was copied. This could have
                 * been because this request is being intentionally
                 * canceled.
                 */
                throw new DropboxPartialFileException(totalRead);
            }
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {}
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {}
            }
            // This will also abort/finish the request if the download is
            // canceled early.
            try {
                close();
            } catch (IOException e) {}
        }
    }
}